<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the PDODB_Connection class.
 *
 * PHP version 5
 *
 * This file is part of PEAR::PDODB - A database abstraction layer for PHP5 built
 * using PDO. PHP version 5.1+ is recommended.
 *
 * PEAR::PDODB is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * PEAR::PDODB is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with PEAR::PDODB. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package     PDODB
 * @category    Database
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007 Michael J. I. Jackson
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version     SVN: $Id: Connection.php 14 2007-12-10 22:58:02Z mjijackson $
 * @link        http://pear.php.net/packages/PDODB
 */

/**
 * Require PDODB_Connection_Exception
 */
require_once str_replace('.php', '', __FILE__) . DIRECTORY_SEPARATOR . 'Exception.php';

/**
 * A base driver class for PDODB database connections. Should be extended by
 * DBMS-specific child classes for each individual driver.
 *
 * @package     PDODB
 * @category    Database
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
abstract class PDODB_Connection
{

    /**
     * This connection's PDO instance.
     *
     * @var     PDO
     * @access  protected
     */
    protected $pdo;

    /**
     * A map of options for this connection. The following list contains a
     * description of each option:
     *
     * <ul>
     * <li>ATTR_EVENT_LISTENER => (PDODB_Listener) An event listener that will
     * listen for events on this connection.</li>
     * <li>ATTR_FETCH_CLASS => (string) The name of a class to use by default when
     * fetching objects from PDODB_Statement result sets. This can be overridden
     * easily on a case-by-case basis in {@link PDODB_Statement::fetchObject()}.</li>
     * <li>ATTR_SILENT_WARNINGS => (bool) True to silence all non-fatal connection
     * warnings. Defaults to false.</li>
     * <li>ATTR_IDXNAME_FORMAT => (string) The default format to use for new
     * index names. Defaults to '%s_idx'.</li>
     * <li>ATTR_SEQNAME_FORMAT => (string) The default format to use for new
     * sequences. Defaults to '%s_seq'.</li>
     * <li>ATTR_SEQCOL_NAME => (string) The default column name to use for newly
     * created sequences. Defaults to 'sequence'.</li>
     * <li>ATTR_DEFAULT_TABLE_TYPE => (string) The default table type to use for a
     * new table created with PDODB_Manager::createTable() whose type is not
     * explicitly specified.</li>
     * <li>ATTR_DEFAULT_DECIMAL_PLACES => (int) The default number of decimal places
     * to use for decimal type fields created with PDODB_Manager::createTable()
     * whose length is not explicitly specified.</li>
     * <li>ATTR_DEFAULT_TEXT_LENGTH => (int) The default length to use for text
     * type fields created with PDODB_Manager::createTable() whose length is not
     * explicitly specified. Defaults to the maximum length allowed by the
     * DBMS.</li>
     * <li>ATTR_QUOTE_IDENTIFIERS => (bool) True to automatically quote all
     * table and field names used in queries.</li>
     * <li>ATTR_MODULES => (array) A map of short to long names of module classes.
     * The short module names may be used in place of the long module names when
     * using modules.</li>
     * </ul>
     *
     * Short module names allow loading of module classes with less typing. For
     * example, instead of:
     *
     * <code>
     * echo $db->queryOne('SELECT ' . $db->function->now('timestamp'));
     * </code>
     *
     * the following may be used:
     *
     * <code>
     * echo $db->queryOne('SELECT ' . $db->fn->now('timestamp'));
     * </code>
     *
     * @var     array
     * @access  protected
     */
    protected $options = array(
        PDODB::ATTR_EVENT_LISTENER          => null,
        PDODB::ATTR_FETCH_CLASS             => 'stdClass',
        PDODB::ATTR_SILENT_WARNINGS         => false,
        PDODB::ATTR_IDXNAME_FORMAT          => '%s_idx',
        PDODB::ATTR_SEQNAME_FORMAT          => '%s_seq',
        PDODB::ATTR_SEQCOL_NAME             => 'sequence',
        PDODB::ATTR_DEFAULT_TABLE_TYPE      => null,
        PDODB::ATTR_DEFAULT_DECIMAL_PLACES  => 2,
        PDODB::ATTR_DEFAULT_TEXT_LENGTH     => 4096,
        PDODB::ATTR_QUOTE_IDENTIFIERS       => false,
        PDODB::ATTR_MODULES                 => array(
            'dt' => 'Datatype',
            'mg' => 'Manager',
            'na' => 'Native',
            'fn' => 'Function'
        )
    );

    /**
     * A map of identifier quote characters.
     *
     * @var     array
     * @access  protected
     */
    protected $id_quotes = array(
        'start'     => "'",
        'end'       => "'",
        'escape'    => ''
    );

    /**
     * A map of features supported by this connection.
     *
     * @var     array
     * @access  protected
     */
    protected $supports = array(
        'sub_queries'   => false,
        'limit_queries' => false
    );

    /**
     * An array of warning messages associated with this connection. Warnings are
     * non-fatal errors that are corrected on the fly, but are useful for
     * debugging purposes.
     *
     * @var     array
     * @access  protected
     */
    protected $warnings = array();

    /**
     * The index of this connection instance in PDODB::$instances.
     *
     * @var     int
     * @access  protected
     */
    protected $instance_index;

    /**
     * The last query performed by this connection.
     *
     * @var     string
     * @access  protected
     */
    protected $last_query;

    /**
     * An array of modules loaded for use with this connection.
     *
     * @var     array
     * @access  protected
     */
    protected $modules = array();

    /**
     * The limit to use on this connection's next query.
     *
     * @var     int
     * @access  protected
     */
    protected $limit = 0;

    /**
     * The offset to use on this connection's next query.
     *
     * @var     int
     * @access  protected
     */
    protected $offset = 0;

    /**
     * An array of PDO connection parameters. Keys are 'dsn', 'user', & 'pass'.
     * Used in {@link connect()} to instantiate a database connection.
     *
     * @var     array
     * @access  private
     */
    private $pdo_params;

    /**
     * An array of connection attributes. Used in {@link connect()} to
     * instantiate a database connection.
     *
     * @var     array
     * @access  private
     */
    private $pdo_attributes;

    /**
     * Constructor. This function should never be called directly. Instead, use
     * {@link PDODB::factory()} to generate PDODB connection instances.
     *
     * @param   mixed   $pdo        A PDO instance or an array of PDO connection
     *                              parameters (for lazy connecting)
     * @param   array   $options    An array of driver-specific options to use
     *                              for this database handle
     * @param   int     $index      The index of this object in PDODB::$instances
     * @access  public
     */
    public function __construct($pdo, $options, $index)
    {
        $this->instance_index = $index;

        if ($pdo instanceof PDO) {
            $this->pdo = $pdo;
            $this->init();
        } else {
            $this->pdo_params = $pdo;
        }

        foreach ($options as $k => $v) {
            $this->setAttribute($k, $v);
        }
    }

    /**
     * Gets the global id of this connection which is just the index of this
     * connection instance in the PDODB::$instances array. Useful for retrieving
     * the SAME connection instance later with {@link PDODB::getConnection()}.
     *
     * @return  int             The index of this connection
     * @access  public
     */
    public function getId()
    {
        return $this->instance_index;
    }

    /**
     * Initializes this connection's PDO database connection if it's not already
     * done. Will automatically be called when needed.
     *
     * @return  void
     * @throws  PDODB_ConnectException      If the connection fails
     * @access  private
     */
    private function connect()
    {
        if ($this->pdo) {
            return;
        }

        $this->listener()->preConnect($this);

        try {
            $this->pdo = new PDO(
                $this->pdo_params['dsn'],
                $this->pdo_params['user'],
                $this->pdo_params['pass']
            );
        } catch (PDOException $e) {
            $code = $this->errorCode($e);
            $cause = PDODB_Connection_Exception::factory($code, $e);
            throw new PDODB_ConnectException($cause);
        }

        foreach ($this->pdo_attributes as $attr => $value) {
            $this->setAttribute($attr, $value);
        }

        $this->init();

        $this->listener()->postConnect($this);
    }

    /**
     * Closes this driver's connection to the database.
     *
     * @return  void
     * @access  public
     */
    public function disconnect()
    {
        $this->listener()->preDisconnect($this);

        unset($this->pdo);

        $this->listener()->postDisconnect($this);
    }

    /**
     * Used to load modules transparently.
     *
     * <code>
     * $conn = PDODB::factory('dsn', 'user', 'pass');
     * $conn->manager; // The manager module, ready to use
     * </code>
     *
     * @param   string  $property       The requested property name
     * @return  mixed                   The corresponding module
     * @throws  PDODB_LoadModuleException       If the specified module fails
     *                                          to load
     * @access  public
     */
    public function __get($property)
    {
        return $this->loadModule($property);
    }

    /**
     * Used to transparently call methods of the underlying PDO instance and
     * loaded modules.
     *
     * <code>
     * $conn = PDODB::factory('dsn', 'user', 'pass');
     * $conn->loadModule('manager');
     * $conn->createTable( ... ); // createTable() is a manager module method
     * </code>
     *
     * @param   string  $method         The called method
     * @param   array   $args           The passed arguments
     * @return  mixed                   The result of the function call
     * @access  public
     */
    public function __call($method, $args)
    {
        foreach ($this->modules as $module) {
            if (method_exists($module, $method)) {
                return call_user_func_array(array($module, $method), $args);
            }
        }

        trigger_error(sprintf('Call to undefined method %s::%s()',
            get_class($this), $method), E_USER_ERROR);
    }

    /**
     * Called immediately after this connection establishes a connection to the
     * database and before any queries are performed. Should be overridden in
     * child classes to initialize class $supports variables.
     *
     * @return  void
     * @access  public
     * @abstract
     */
    abstract public function init();

    /**
     * Gets the PDODB error code from a PDOException that was thrown. This
     * function overrides {@link http://php.net/manual/function.PDO-errorCode PDO::errorCode()}
     * and standardizes error codes across different DBMS. Should be overridden
     * in child classes to return the appropriate error code for the given
     * exception.
     *
     * @param   PDOException    $e      The exception that was thrown
     * @return  int                     One of the PDODB::E_* constants
     * @access  public
     * @abstract
     */
    public function errorCode(PDOException $e)
    {
        return PDODB::E_UNKNOWN;
    }

    /**
     * Modifies a SQL query string for various DBMS specific reasons. Should be
     * overridden in child classes to modify the query accordingly.
     *
     * @param   string  $query          The query to modify
     * @param   bool    $is_manip       True if it is a DML query
     * @param   int     $limit          The limit to set on the query
     * @param   int     $offset         The offset to start reading from
     * @return  string                  The modified query
     * @access  protected
     */
    protected function modifyQuery($query, $is_manip, $limit, $offset)
    {
        return $query;
    }

    /**
     * Used primarily by internal methods to contain non-fatal errors for this
     * connection. Handy for debugging.
     *
     * @param   string  $class      The class in which the error occured
     * @param   string  $function   The function in which the error occured
     * @param   string  $msg        The error message
     * @return  void
     * @access  public
     */
    public function raiseWarning($class, $function, $msg)
    {
        $warning = sprintf('[%s::%s()] %s', $class, $function, $msg);
        $this->warnings[] = $warning;

        $this->listener()->onWarning($this);

        if (!$this->getAttribute(PDODB::ATTR_SILENT_WARNINGS)) {
            trigger_error($warning, E_USER_WARNING);
        }
    }

    /**
     * Gets an array containing this connection's {@link $warnings} in reverse
     * order. This means that the last warning issued will be first in the
     * resulting array.
     *
     * @return  array           An array of this connection's warning messages
     * @access  public
     */
    public function getWarnings()
    {
        return array_reverse($this->warnings);
    }

    /**
     * Resets this connection's internal array of {@link $warnings warnings}.
     *
     * @return  void
     * @access  public
     */
    public function resetWarnings()
    {
        $this->warnings = array();
    }

    /**
     * Determines whether this connection supports the specified feature or not.
     *
     * @param   string  $feature        The feature name
     * @return  bool                    True if the feature is supported
     * @throws  PDODB_Exception         If the feature is unknown
     * @access  public
     * @see     $supports
     */
    public function supports($feature)
    {
        if (array_key_exists($feature, $this->supports)) {
            return $this->supports[$feature];
        }

        throw new PDODB_Exception("Unknown support feature '$feature'");
    }

    /**
     * Gets the value of the given attribute.
     *
     * @param   int     $attribute      One of the PDO::ATTR_* or PDODB::ATTR_*
     *                                  constants
     * @return  mixed                   The value of the attribute, NULL if the
     *                                  attribute is not known
     * @access  public
     */
    public function getAttribute($attribute)
    {
        if ($attribute < 100) {
            $this->connect();
            return $this->pdo->getAttribute($attribute);
        }
        if (array_key_exists($attribute, $this->options)) {
            return $this->options[$attribute];
        }

        return null;
    }

    /**
     * Used to intercept attempts to change the error mode of this connection.
     * PDODB requires all connection error modes to be PDO::ERRMODE_EXCEPTION.
     *
     * @param   int     $attribute      One of the PDO::ATTR_* or PDODB::ATTR_*
     *                                  constants
     * @param   mixed   $value          The value to set it to
     * @return  bool                    True on success, false on failure
     * @throws  PDODB_Exception         If the specified attribute is
     *                                  PDO::ATTR_ERRMODE and the value is
     *                                  something other than PDO::ERRMODE_EXCEPTION
     * @access  public
     */
    public function setAttribute($attribute, $value)
    {
        if ($attribute == PDO::ATTR_ERRMODE && $value != PDO::ERRMODE_EXCEPTION) {
            throw new PDODB_Exception('PDODB requires all drivers to use PDO::ERRMODE_EXCEPTION');
        }
        if ($attribute < 100) {
            if ($this->pdo) {
                return $this->pdo->setAttribute($attribute, $value);
            } else {
                $this->pdo_attributes[$attribute] = $value;
                return true;
            }
        }
        if (array_key_exists($attribute, $this->options)) {
            $this->options[$attribute] = $value;
            return true;
        }

        return false;
    }

    /**
     * Loads a module for use with this connection. The module instance will be
     * stored in a class variable with the same name. Example:
     *
     * <code>
     * $conn = PDODB::factory('dsn', 'user', 'pass');
     * $conn->loadModule('manager'); // Loads the manager module
     * $conn->manager->createTable( ... );
     * </code>
     *
     * @param   string  $module             The name of the module to load
     * @param   bool    $driver_specific    True if the module has different
     *                                      implementations for different drivers
     * @return  bool                        True on success, false on failure
     * @throws  PDODB_ModuleException       If the module fails to load properly
     * @access  public
     */
    public function loadModule($module, $driver_specific = null)
    {
        $name = strtolower($module);

        if (isset($this->options[PDODB::ATTR_MODULES][$name])) { // short name
            $module = $this->options[PDODB::ATTR_MODULES][$name];
            $name = strtolower($module);
        } else {
            $module = ucfirst($name);
        }

        if (!isset($this->modules[$name])) {
            try {
                $mod_cls = "PDODB_$module";
                if (!is_bool($driver_specific)
                    && is_dir(dirname(__FILE__) . DIRECTORY_SEPARATOR . $module)) {
                        $driver_specific = true;
                }
                if ($driver_specific) {
                    $mod_cls .= '_' . $this->driver();
                }
                PDODB::loadClass($mod_cls);
            } catch (PDODB_Exception $e) {
                throw new PDODB_LoadModuleException($mod_cls, $e);
            }

            $this->modules[$name] = new $mod_cls($this->getId());
        }

        return $this->modules[$name];
    }

    /**
     * Gets this connection's event listener.
     *
     * @return  PDODB_Listener  This connection's event listener
     * @access  public
     */
    public function listener()
    {
        $listener = $this->getAttribute(PDODB::ATTR_EVENT_LISTENER);

        if (!$listener) {
            PDODB::loadClass('PDODB_Listener_Default');
            $listener = new PDODB_Listener_Default();
            $this->setAttribute(PDODB::ATTR_EVENT_LISTENER, $listener);
        }

        return $listener;
    }

    /**
     * Gets the name of the database driver.
     *
     * @return  string          The name of the driver in use
     * @access  public
     */
    public function driver()
    {
        return $this->getAttribute(PDO::ATTR_DRIVER_NAME);
    }

    /**
     * Gets the version number of the underlying database server.
     *
     * @return  string          The version number of the DBMS
     * @access  public
     */
    public function getServerVersion()
    {
        return $this->getAttribute(PDO::ATTR_SERVER_VERSION);
    }

    /**
     * Quotes the given value for safe use in an SQL statement. Note: arrays and
     * objects passed to this function will be {@link http://php.net/serialize serialized}
     * before quoting.
     *
     * @param   mixed   $value      The value to quote
     * @param   string  $type       The type of the value
     * @return  string              The quoted value
     * @access  public
     */
    public function quote($value, $type = null)
    {
        if (is_null($type)) {
            $type = gettype($value);
        }

        switch ($type) {
            case 'integer':
            case 'enum':
            case 'boolean':
            case 'double':
            case 'float':
            case 'bool':
            case 'int':
                return $value;
            case 'array':
            case 'object':
                $value = serialize($value);
            case 'string':
            case 'char':
            case 'varchar':
            case 'text':
            case 'gzip':
            case 'blob':
            case 'clob':
                $this->connect();
                return $this->pdo->quote($value);
        }
    }

    /**
     * Quotes a table or field name for use in an SQL statement.
     *
     * @param   string  $identifier     The identifier to use
     * @param   bool    $check_option   True to check this connection's
     *                                  ATTR_QUOTE_IDENTIFIERS attribute
     * @return  string                  The identifier quoted
     * @access  public
     */
    public function quoteIdentifier($identifier, $check_option = false)
    {
        if ($check_option && !$this->getAttribute(PDODB::ATTR_QUOTE_IDENTIFIERS)) {
            return $identifier;
        }

        extract($this->id_quotes);
        return $start . str_replace($end, $escape . $end, $identifier) . $end;
    }

    /**
     * Formats a sequence name according to this connection's 'seqname_format'
     * option.
     *
     * @param   string  $name   The name of the sequence
     * @return  string          The formatted sequence name
     * @access  public
     */
    public function sequenceName($name)
    {
        return sprintf($this->getAttribute(PDODB::ATTR_SEQNAME_FORMAT),
            preg_replace('/[^a-z0-9_\$.]/i', '_', $name));
    }

    /**
     * Removes any formatting in a sequence name using the 'seqname_format'
     * driver option.
     *
     * @param   string      $name   The name of a potential sequence
     * @param   bool        $check  True to return only formatted sequences
     * @return  string              The name of the sequence with possible
     *                              formatting removed
     * @access  protected
     */
    protected function fixSequenceName($name, $check = false)
    {
        $seq_pattern = '/^' . preg_replace('/%s/', '([a-z0-9_]+)',
            $this->getAttribute(PDODB::ATTR_SEQNAME_FORMAT)) . '$/i';
        $seq_name = preg_replace($seq_pattern, '\\1', $name);
        if ($seq_name && !strcasecmp($name, $this->sequenceName($seq_name))) {
            return $seq_name;
        }
        return $check ? false : $name;
    }

    /**
     * Formats an index name according to this connection's 'idxname_format'
     * option.
     *
     * @param   string  $name       The name of the index
     * @return  string              The formatted index name
     * @access  public
     */
    public function indexName($name)
    {
        return sprintf($this->getAttribute(PDODB::ATTR_IDXNAME_FORMAT),
            preg_replace('/[^a-z0-9_\$]/i', '_', $name));
    }

    /**
     * Removes any formatting in an index name using the 'idxname_format'
     * connection option.
     *
     * @param   string      $name   The name of a potential index
     * @return  string              The name of the index with possible
     *                              formatting removed
     * @access  protected
     */
    protected function fixIndexName($name)
    {
        $idx_pattern = '/^' . preg_replace('/%s/', '([a-z0-9_]+)',
            $this->getAttribute(PDODB::ATTR_IDXNAME_FORMAT)) . '$/i';
        $idx_name = preg_replace($idx_pattern, '\\1', $name);
        if ($idx_name && !strcasecmp($name, $this->indexName($idx_name))) {
            return $idx_name;
        }
        return $name;
    }

    /**
     * Sets the limit/offset to use in the next query.
     *
     * @param   int     $limit      The number of rows to select
     * @param   int     $offset     The number of the first row to select
     * @return  void
     * @throws  PDODB_UnsupportedFeatureException   If the driver doesn't support
     *                                              limit queries
     * @throws  PDODB_InvalidRangeException         If the limit or offset values
     *                                              are negative
     * @access  public
     */
    public function setLimit($limit, $offset = null)
    {
        $this->connect();

        if (!$this->supports('limit_queries')) {
            throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
        }

        $limit = (int)$limit;
        if ($limit < 0) {
            throw new PDODB_InvalidRangeException("Limit $limit is not valid");
        }
        $this->limit = $limit;

        if (!is_null($offset)) {
            $offset = (int)$offset;
            if ($offset < 0) {
                throw new PDODB_InvalidRangeException("Offset $offset is not valid");
            }
            $this->offset = $offset;
        }
    }

    /**
     * Resets limit and offset parameters to 0 for the next query.
     *
     * @return  void
     * @access  public
     */
    public function resetLimit()
    {
        $this->limit = 0;
        $this->offset = 0;
    }

    /**
     * Initiates a transaction. See
     * {@link http://php.net/manual/function.PDO-beginTransaction PDO::beginTransaction()}
     * for more information.
     *
     * @return  bool        True on success, false on failure
     * @access  public
     */
    public function beginTransaction()
    {
        $this->connect();
        return $this->pdo->beginTransaction();
    }

    /**
     * Executes a manipulative query on the database. See the
     * {@link http://php.net/manual/function.PDO-exec.php PDO::exec()}
     * documentation for a detailed description of this function's parameters.
     *
     * @return  int                     The number of affected rows
     * @throws  PDODB_Connection_Exception  If the query fails
     * @access  public
     */
    public function exec()
    {
        $this->connect();

        $this->listener()->preExec($this);

        $args = func_get_args();
        $args[0] = $this->modifyQuery($args[0], true, $this->limit, $this->offset);
        $this->last_query = $args[0];
        $this->resetLimit();

        try {
            $result = call_user_func_array(array($this->pdo, 'exec'), $args);
        } catch (PDOException $e) {
            $code = $this->errorCode($e);
            throw PDODB_Connection_Exception::factory($code, $e, $args[0]);
        }

        $this->listener()->postExec($this);

        return $result;
    }

    /**
     * Creates a {@link http://php.net/manual/function.PDO-prepare prepared}
     * statement.
     *
     * @param   string  $statement      The statement to prepare
     * @return  PDODB_Statement         The prepared statement
     * @access  public
     */
    public function prepare($statement)
    {
        $this->connect();

        $this->listener()->prePrepare($this);

        $prepared = $this->pdo->prepare($statement);

        $this->listener()->postPrepare($this);

        return $prepared;
    }

    /**
     * Queries the database. See the
     * {@link http://php.net/manual/function.PDO-query.php PDO::query()}
     * documentation for a detailed description of this function's parameters.
     *
     * @return  mixed                   The result of the query
     * @throws  PDODB_Connection_Exception  If the query fails
     * @access  public
     */
    public function query()
    {
        $this->connect();

        $this->listener()->preQuery($this);

        $args = func_get_args();
        $args[0] = $this->modifyQuery($args[0], false, $this->limit, $this->offset);
        $this->last_query = $args[0];
        $this->resetLimit();

        try {
            $result = call_user_func_array(array($this->pdo, 'query'), $args);
        } catch (PDOException $e) {
            $code = $this->errorCode($e);
            throw PDODB_Connection_Exception::factory($code, $e, $args[0]);
        }

        $this->listener()->postQuery($this);

        return $result;
    }

    /**
     * Returns a string suitable to use in an SQL sub-query. If the DBMS
     * supports sub-queries, the query will be returned untouched. If not, the
     * query will be executed and the resulting values will be returned in a
     * comma-separated list.
     *
     * @param   string  $sql            The sub-query to use
     * @return  string                  A string to use in the sub-query
     * @throws  PDODB_Connection_Exception  If the sub-query fails
     * @access  public
     */
    public function subQuery($sql)
    {
        if ($this->supports('sub_queries')) {
            return $sql;
        }

        $col = $this->queryCol($sql);
        return count($col) === 0 ? 'NULL' : implode(', ', $col);
    }

    /**
     * Gets the last query executed with this connection.
     *
     * @return  string          The last query
     * @access  public
     */
    public function lastQuery()
    {
        return $this->last_query;
    }

    /**
     * Executes the query, fetches the value from the specified column number
     * of the first row of the result set, frees the result and returns the
     * value.
     *
     * @param   string  $sql            The query to execute
     * @param   int     $column         The column number to fetch
     * @return  mixed                   The fetched value
     * @throws  PDODB_Connection_Exception  If the query fails
     * @access  public
     */
    public function queryOne($sql, $column = 0)
    {
        $stmt = $this->query($sql);
        $one = $stmt->fetchColumn($column);
        $stmt->free();
        return $one;
    }

    /**
     * Executes the query, fetches the specified column index from each row of
     * the result set into an array, frees the result and returns the array.
     *
     * @param   string  $sql            The query to execute
     * @param   int     $column         The column number to fetch from each row
     * @return  array                   An array of the fetched values
     * @throws  PDODB_Connection_Exception  If the query fails
     * @access  public
     */
    public function queryCol($sql, $column = 0)
    {
        $stmt = $this->query($sql);
        $values = array();
        while ($col = $stmt->fetchColumn($column)) {
            $values[] = $col;
        }
        $stmt->free();
        return $values;
    }

    /**
     * Executes the query, fetches the first row from the result set, frees the
     * result and returns the first row.
     *
     * @param   string  $sql            The query to execute
     * @return  array                   An associative array containing the first
     *                                  row of the result set
     * @throws  PDODB_Connection_Exception  If the query fails
     * @access  public
     */
    public function queryRow($sql)
    {
        $stmt = $this->query($sql);
        $row = $stmt->rowArray();
        $stmt->free();
        return $row;
    }

    /**
     * Executes the query, fetches the first row from the result set, frees the
     * result and returns the first row as an object of the given class. If no
     * class name is given, stdClass will be used.
     *
     * @param   string  $sql            The query to execute
     * @param   string  $class_name     The name of the class to use for the
     *                                  resulting object
     * @param   array   $ctor_args      An array of arguments to pass to the
     *                                  constructor
     * @return  object                  An object whose members correspond to
     *                                  those of the first row in the result set
     * @throws  PDODB_Connection_Exception  If the query fails
     * @access  public
     */
    public function queryObject($sql, $class_name = 'stdClass',
        $ctor_args = array())
    {
        $stmt = $this->query($sql);
        $obj = $stmt->fetchObject($class_name, $ctor_args);
        $stmt->free();
        return $obj;
    }

    /**
     * Executes the query and fetches all rows of the result into an array. This
     * function accepts a variable-length list of arguments. The first parameter
     * is the SQL query to execute. The remaining parameters will be passed to
     * the resulting statement's
     * {@link http://php.net/manual/function.PDOStatement-fetchAll fetchAll()}
     * method. For example, the following will fetch all results into an array
     * of objects of type stdClass:
     *
     * <code>
     * queryAll('SELECT * FROM table', PDO::FETCH_OBJ);
     * </code>
     *
     * Likewise, the following will fetch only the unique values of the first
     * column in the result set:
     *
     * <code>
     * queryAll('SELECT * FROM table', PDO::FETCH_COLUMN | PDO::FETCH_UNIQUE, 0);
     * </code>
     *
     * @param   string  $sql            The query to execute
     * @param   int     $fetch_mode     The fetch mode to use
     * @return  array                   A two-dimensional array of the result
     * @access  public
     */
    public function queryAll($sql, $fetch_mode = PDO::FETCH_ASSOC)
    {
        $args = func_get_args();
        $stmt = $this->query(array_shift($args));
        $args[0] = $fetch_mode;
        $all = call_user_func_array(array($stmt, 'fetchAll'), $args);
        $stmt->free();
        return $all;
    }

    /**
     * Returns the name of the current database.
     *
     * @return  string              The name of the current database in use
     * @throws  PDODB_UnsupportedFeatureException   If the connection doesn't support
     *                                              the use of this function
     * @access  public
     */
    public function getDatabase()
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Sets the database to use for queries. Note: This does not need to be
     * called if the database name was specified in the DSN.
     *
     * @param   string  $name       The name of the database to use
     * @return  string              The name of the last database in use
     * @throws  PDODB_UnsupportedFeatureException   If the connection doesn't support
     *                                              the use of this function
     * @access  public
     */
    public function setDatabase($name)
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Gets a list of all databases.
     *
     * @return  array               The names of all the databases
     * @throws  PDODB_UnsupportedFeatureException   If the connection doesn't support
     *                                              the use of this function
     * @access  public
     */
    public function listDatabases()
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Determines whether the specified database exists.
     *
     * @param   string  $name       The name of the database to check for
     * @return  bool                True if it exists, false otherwise
     * @access  public
     */
    public function databaseExists($name)
    {
        return in_array($name, $this->listDatabases());
    }

    /**
     * Gets a list of all the tables in the current database.
     *
     * @return  array               The names of all the database tables
     * @throws  PDODB_UnsupportedFeatureException   If the connection doesn't support
     *                                              the use of this function
     * @access  public
     */
    public function listTables()
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Determines if the specified table exists in the database.
     *
     * @param   string  $table      The table to check for
     * @return  bool                True if the table exists, false otherwise
     * @access  public
     */
    public function tableExists($table)
    {
        return in_array($table, $this->listTables());
    }

    /**
     * Gets a list of the columns in a database table.
     *
     * @param   string  $table      The database table name
     * @return  array               The names of the table's columns
     * @throws  PDODB_UnsupportedFeatureException   If the connection doesn't support
     *                                              the use of this function
     * @access  public
     */
    public function listColumns($table)
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Determines if the specified column exists in the database table.
     *
     * @param   string  $column     The column to check for
     * @param   string  $table      The table to look in
     * @return  bool                True if the column exists, false otherwise
     * @access  public
     */
    public function columnExists($column, $table)
    {
        return in_array($column, $this->listColumns($table));
    }

    /**
     * Gets a list of all views in the current database.
     *
     * @return  array               The names of all the database views
     * @throws  PDODB_UnsupportedFeatureException   If the connection doesn't support
     *                                              the use of this function
     * @access  public
     */
    public function listViews()
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Gets a list of all indexes in the given table.
     *
     * @param   string  $table      The name of the table
     * @return  array               An array of index names
     * @access  public
     */
    public function listIndexes($table)
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Gets a list of all constraints in the given table.
     *
     * @param   string  $table      The name of the table
     * @return  array               An array of constraint names
     * @access  public
     */
    public function listConstraints($table)
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Gets a list of all sequences in the current database.
     *
     * @return  array               An array of sequence names
     * @access  public
     */
    public function listSequences()
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Gets a list of all DBMS functions.
     *
     * @return  array               An array of all DBMS function names
     * @access  public
     */
    public function listFunctions()
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Gets a list of database users.
     *
     * @return  array               The names of all database users
     * @throws  PDODB_UnsupportedFeatureException   If the connection doesn't support
     *                                              the use of this function
     * @access  public
     */
    public function listUsers()
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Gets the next free ID in a sequence.
     *
     * @param   string  $seq_name   The name of the sequence
     * @param   bool    $ondemand   True to create the sequence if it doesn't
     *                              already exist
     * @return  int                 The next ID in the sequence
     * @access  public
     */
    public function nextId($seq_name, $ondemand = true)
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Returns the current ID of a sequence.
     *
     * @param   string  $seq_name   The name of the sequence
     * @return  int                 The current ID in the sequence
     * @access  public
     */
    public function currId($seq_name)
    {
        throw new PDODB_UnsupportedFeatureException(__FUNCTION__, $this->driver());
    }

    /**
     * Creates a new {@link PDODB_Query} object with this connection as the
     * {@link PDODB_Query::setDefaultConnection() default} and passes all
     * parameters to the query's {@link PDODB_Query::select() select} method.
     * Usage:
     *
     * <code>
     * $stmt = $db->select('*', 'people')->execute();
     * </code>
     *
     * @param   mixed       $fields     The field(s) to select
     * @param   string      $tables     The table(s) to use
     * @return  PDODB_Query             The query object
     * @access  public
     */
    public function select($fields, $tables)
    {
        PDODB::loadClass('PDODB_Query');

        $query = new PDODB_Query($this);
        return $query->select($fields, $tables);
    }

    /**
     * Creates a new {@link PDODB_Query} object with this connection as the
     * {@link PDODB_Query::setDefaultConnection() default} and passes all
     * parameters to the query's {@link PDODB_Query::insert() insert} method.
     *
     * @param   string  $table          The table name
     * @param   mixed   $data           An array or object of data to insert
     * @return  PDODB_Query             The query object
     * @access  public
     */
    public function insert($table, $data)
    {
        PDODB::loadClass('PDODB_Query');

        $query = new PDODB_Query($this);
        return $query->insert($table, $data);
    }

    /**
     * Creates a new {@link PDODB_Query} object with this connection as the
     * {@link PDODB_Query::setDefaultConnection() default} and passes all
     * parameters to the query's {@link PDODB_Query::update() update} method.
     * Usage:
     *
     * <code>
     * $data = array(
     *     'senior' => 1
     * );
     * $affected_rows = $db->update('people', $data)->where('age >', 62)->execute();
     * </code>
     *
     * @param   mixed   $table          The table name
     * @param   mixed   $data           An array or object of data to update
     * @return  PDODB_Query             The query object
     * @access  public
     */
    public function update($table, $data)
    {
        PDODB::loadClass('PDODB_Query');

        $query = new PDODB_Query($this);
        return $query->update($table, $data);
    }

    /**
     * Creates a new {@link PDODB_Query} object with this connection as the
     * {@link PDODB_Query::setDefaultConnection() default} and passes all
     * parameters to the query's {@link PDODB_Query::delete() delete} method.
     *
     * @param   string      $table      The table to use
     * @return  PDODB_Query             The query object
     * @access  public
     */
    public function delete($table)
    {
        PDODB::loadClass('PDODB_Query');

        $query = new PDODB_Query($this);
        return $query->delete($table);
    }

}

?>
